4.02. Операторы
Разработчику
Аналитику
Тестировщику
Архитектору
Инженеру
Операторы
Приоритеты и ассоциативность
В коде есть одна важная штука - операторы. К примеру, мы ранее уже упомянули условные операторы. Но сами по себе операторы бывают разными, и они есть всегда.
Каждое действие, технически - это операция. Операция производится при помощи оператора, между операндами. Самое необычное, кстати, по части операторов, наблюдается в языке программирования JavaScript, поэтому мы порой будем наблюдать примеры оттуда.
Оператор - это символ или ключевое слово, которое указывает компилятору или интерпретатору выполнить определённое действие над одним или несколькими операндами. Пример - a + b, где + - оператор, a, b - операнды.
Операторы работают с данными, и этими данными и являются операнды. Количество операндов определяет арность оператора, то есть сколько аргументов он принимает.
Унарные операторы (один операнд) действуют на одно значение. Они часто используются для изменения знака, инкремента, проверки логического значения и т.п.
К примеру:
-x— унарный минус: меняет знак числа.+x— унарный плюс: приводит значение к числу (в JS: +'5' → 5).!x— логическое отрицание: !true → false.++xилиx++— инкремент (увеличение на 1).--x илиx--` — декремент.typeof x(в JavaScript) — возвращает тип операнда.~x— побитовое отрицание (инвертирует все биты).
Унарные операторы могут быть префиксными (++x) или постфиксными (x++). В C/JS/Java и других языках постфиксная форма возвращает старое значение, префиксная — новое.
Бинарные операторы (два операнда) это наиболее распространённый тип. Оператор «связывает» два значения.
Примеры:
- Арифметика:
a + b,x * y - Присваивание:
a = 5 - Сравнение:
x > y,a == b - Логика:
a && b,x || y - Побитовые:
a & b,x ^ y - Конкатенация строк:
"Hello" + "world"(в JS, Python)
Порядок операндов может влиять на результат, к примеру, деление.
Тернарный оператор (три операнда), в большинстве языков только один - условный:
условие ? значение_если_истина : значение_если_ложь
Некоторые языки (например, Ruby) не имеют тернарного оператора в таком виде, но поддерживают сокращённые формы (if в выражениях).
Когда в выражении несколько операторов, важно знать порядок их выполнения. Это регулируется двумя правилами: приоритетом и ассоциативностью.
Почему a + b * c ≠ (a + b) * c?
Потому что у оператора * приоритет выше, чем у +. Следовательно, a + b * c интерпретируется как a + (b * c). Если сомневаетесь в порядке - всегда ставьте скобки.
Можно сделать некую обобщённую таблицу приоритетов операторов:
| Уровень | Операторы | Описание |
|---|---|---|
| 1 (высший) | () [] . ?. new | Вызов, доступ, создание |
| 2 | ++ -- + - ! ~ typeof | Унарные операторы |
| 3 | ** | Возведение в степень (справа налево) |
| 4 | * / % | Умножение, деление, остаток |
| 5 | + - | Сложение, вычитание |
| 6 | << >> >>> | Побитовые сдвиги |
| 7 | < <= > >= in instanceof | Сравнение, проверка |
| 8 | == != === !== | Равенство |
| 9 | & | Побитовое И |
| 10 | ^ | Побитовое XOR |
| 11 | ` | ` |
| 12 | && | Логическое И |
| 13 | ` | |
| 14 | ?: | Тернарный оператор |
| 15 | = += -= | Присваивание |
| 16 (низший) | , | Запятая (разделение выражений) |
Важно: приоритет может отличаться в разных языках. Например, в Python ** имеет более высокий приоритет, чем унарный -: -2**2 → -4, а не 4.
Ассоциативность определяет, в каком порядке выполняются операторы одинакового приоритета.
Пример левой ассоциативности: a - b - c. Выполняется как (a - b) - c — слева направо.
Пример правой ассоциативности: a = b = c. Выполняется как a = (b = c) — справа налево, потому что присваивание правоассоциативно.
Почему это важно?
let a, b, c;
a = b = c = 5; // Все получат 5
Если бы = было левоассоциативным, это было бы (a = b) = c, что невозможно, потому что результат a = b — значение, а не lvalue.
Другие правоассоциативные операторы:
- Тернарный:
a ? b : c ? d : e → a ? b : (c ? d : e) - Возведение в степень (в JS):
2 ** 3 ** 2 → 2 ** (3 ** 2) = 2 ** 9 = 512
Символы и арифметика
На первый взгляд, операторы — это просто символы. Но на самом деле, многие операторы эквивалентны вызову функций. Это особенно заметно в языках с возможностью перегрузки операторов.
Во многих языках выражение a + b можно представить как вызов функции:
// C++
operator+(a, b)
В функциональных языках (например, Haskell) операторы — это обычные функции, просто с инфиксным синтаксисом:
(+) a b -- то же, что a + b
Даже в Python:
import operator
result = operator.add(a, b) # вместо a + b
Некоторые языки позволяют переопределять поведение операторов для пользовательских типов.
class Vector {
public:
int x, y;
Vector(int x, int y) : x(x), y(y) {}
// Перегрузка оператора +
Vector operator+(const Vector& other) {
return Vector(x + other.x, y + other.y);
}
};
Vector a(1, 2), b(3, 4);
Vector c = a + b; // Работает как будто встроенный тип!
Зачем это нужно? Чтобы пользовательские типы вели себя интуитивно, как встроенные: складывать векторы, сравнивать даты, умножать матрицы.
Где нельзя перегружать операторы?
- В Java: нельзя (кроме + для строк — это особое поведение).
- В JavaScript: нельзя (но можно эмулировать через методы).
- В C#: можно, но с ограничениями (например, нельзя создавать новые символы).
Некоторые операторы не могут быть выражены как функции, потому что изменяют поток выполнения (if, return, throw), создают контекст (new, await), или требуют специальной обработки компилятором.
Операторы — это синтаксический интерфейс к действиям. Они делают код короче, читаемее, выразительнее. Но за этим синтаксисом стоит логика, которую важно понимать. Знание того, что оператор может быть функцией, помогает глубже понять, как работает язык.
Арифметические операторы — это основа вычислений. Они работают с числами и возвращают числовое значение.
| Оператор | Значение | Пример |
|---|---|---|
+ | Сложение | 5+3=8 |
- | Вычитание | 5-3=2 |
* | Умножение | 5*3=15 |
/ | Деление | 6/3=2 |
% | Остаток от деления | 7%3=1 |
** | Возведение в степень (JS, Python) | 2**3=8 |
^ | Не степень! В большинстве языков — побитовое XOR (C, Java, JS) | 5^3=6 |
В большинстве языков при попытке деления на ноль возникает ошибка времени выполнения или исключение. Кроме JavaScript, там не ошибка, а специальное значение - NaN.
Присваивание
Присваивание — это не просто «сохранить значение». Это оператор, который возвращает значение.
Базовое присваивание: = - когда объявляется переменная, единичный знак = означает присваивание ей значения.
let x = 10;
Составные операторы - это присваивающие операторы, которые немного усложняют логику, выполняя дополнительное вычисление с присвоением результата как значения:
| Оператор | Аналог | Пример |
|---|---|---|
+= | a = a + b | x += 5 |
-= | a = a - b | x -= 3 |
*= | a = a * b | x *= 2 |
/= | a = a / b | x /= 4 |
%= | a = a % b | x %= 3 |
**= | a = a ** b | x **= 2 |
let x = 10;
x *= 3; // x = 30
x %= 7; // x = 2
В C, C++, JavaScript, PHP и других языках оператор = возвращает присвоенное значение. Это позволяет писать:
let a, b, c;
a = b = c = 5; // Все получат 5
Но будьте осторожны:
if (x = 5) { ... } // ОШИБКА? Нет, но x станет 5, и условие true!
Частая ошибка новичков: = вместо ==. Современные линтеры это ловят.
Сравнение
Операторы сравнения - ещё один вид операторов. Сравнивают два значения и возвращают булево значение: true или false.
| Оператор | Аналог |
|---|---|
| Оператор Значение | |
== | Равно (с приведением типов) |
!= | Не равно |
=== | Строгое равенство (без приведения) |
!== | Строгое неравенство |
< | Меньше |
> | Больше |
<= | Меньше или равно |
>= | Больше или равно |
JavaScript — классический пример, где разница критична. Потому что == выполняет приведение типов (type coercion). А вот === сравнивает значение и тип.
0 == '' // true
0 == false // true
'' == false // true
0 === '' // false (число vs строка)
0 === false // false
'' === false // false
Как работает приведение?
- Строки преобразуются в числа: '5' == 5 → true
- false → 0, true → 1
- null → 0 (в числовом контексте), undefined → NaN
- Объекты → их примитивное значение (через toString() или valueOf())
Логические операторы работают с булевыми значениями, но возвращают оригинальные значения, а не только true/false.
| Оператор | Значение |
|---|---|
| && | Логическое И |
| ! | Логическое НЕ |
В JavaScript и Python логические операторы возвращают один из операндов:
null || 'default' // → 'default'
'hello' || 'world' // → 'hello'
0 && 'text' // → 0
'hello' && 'world' // → 'world'
Это позволяет писать:
const name = username || 'Аноним';
Побитовые операторы работают с числами на уровне битов. Числа временно преобразуются в 32-битные целые со знаком (в JS), затем обратно.
| Оператор | Значение | Пример |
|---|---|---|
& | Побитовое И | 5 & 3 → 1 |
| | Побитовое ИЛИ | 5 | 3 → 7 |
^ | Исключающее ИЛИ (XOR) | 5 ^ 3 → 6 |
~ | Побитовое НЕ | ~5 → -6 |
Сдвиг и управление потоком
Операторы сдвига сдвигают биты числа влево или вправо.
| Оператор | Значение |
|---|---|
<< | Левый сдвиг (умножение на 2^n) |
>> | Арифметический правый сдвиг (деление, с сохранением знака) |
>>> | Логический правый сдвиг (заполняет нулями) |
Примеры:
8 << 1 // 16 (8 * 2)
8 >> 1 // 4 (8 / 2)
-8 >> 1 // -4 (знак сохраняется)
-8 >>> 1 // 2147483644 (в JS: заполняет нулями, становится положительным!)
В JavaScript все числа — 64-битные double, но побитовые операции работают с 32-битными целыми. >>> используется, чтобы получить беззнаковое представление.
Операторы управления потоком. Хотя некоторые не считают их «операторами» в строгом смысле, по сути — это инструкции, управляющие ходом выполнения программы.
| Оператор | Значение |
|---|---|
| if, else | Условное выполнение |
| switch | Множественный выбор |
| for, while, do-while | Циклы |
| break | Выход из цикла/switch |
| continue | Пропуск итерации |
| return | Возврат из функции |
| throw | Генерация исключения |
Важно - они не являются функциями. К примеру, if - он не возвращает значение, управляет потоком, а не вычисляет выражение, его нельзя передать как аргумент.
Операторы управления потоком ломают линейность программы:
- if — ветвление
- for — повторение
- return — досрочный выход
- throw — переход к обработчику исключений
Специальные операторы
Существуют также специальные операторы, отвечающие за доступ, вызов, безопасность. Эти операторы — не просто инструменты вычислений, а архитектурные элементы, позволяющие работать с объектами, функциями и null-значениями безопасно и элегантно.
Оператор () — один из самых важных в любом языке. Он выполняет функцию.
func(); // вызов функции без аргументов
func(a, b); // с аргументами
obj.method(); // вызов метода объекта
В JavaScript функции — объекты первого класса:
function greet() { return "Hello"; }
console.log(greet); // [Function: greet] — сама функция
console.log(greet()); // "Hello" — результат вызова
() — это оператор вызова, он применяет функцию к аргументам. Без () функция не выполняется — только передаётся. А new + () — создание объекта:
const date = new Date(); // вызов конструктора
const user = new User(name); // создание экземпляра
new — унарный оператор, который создаёт новый объект и вызывает функцию-конструктор.
() — передаёт аргументы конструктору.
Операторы доступа к свойствам: . и [] позволяют получать доступ к свойствам объектов, полям классов, элементам структур.
Точка: obj.property — статический доступ. Используется, когда имя свойства известно заранее. Такой подход используется практически повсеместно - можно написать путь к методу или свойству, разделяя точками:
user.name
car.engine.power
Math.PI
Квадратные скобки: obj["property"] — динамический доступ. Имя свойства — выражение (строка, переменная, результат функции). Полезно, когда имя содержит спецсимволы или определяется во время выполнения.
let key = "name";
user[key]; // эквивалент user.name
user["first-name"]; // допустимо, а user.first-name — ошибка (минус)
let dynamicKey = "age" + suffix;
user[dynamicKey];
А длинные пути называются цепочками доступа - user.profile.settings.theme.
Оператор опциональной цепочки: ?. можно наблюдать в JavaScript (ES2020), C#, Swift и других языках. Решает проблему безопасного доступа к вложенным свойствам.
user?.profile?.settings?.theme
Если user — null или undefined, выражение немедленно возвращает undefined, не пытаясь читать profile. Аналог ручной проверки:
user && user.profile && user.profile.settings && user.profile.settings.theme
Работает только с null и undefined. Если user — примитив (42, true), будет ошибка. Не защищает от user как строки или числа. Варианты использования:
- Методы:
user?.getName?.()— безопасный вызов метода - Массивы:
users?.[0]?.name - Функции:
callback?.(data)— вызов, только если callback существует
Оператор нулевого слияния: ?? решает проблему установки значения по умолчанию, но только если значение — null или undefined.
a ?? b
Возвращает a, если a не null и не undefined. Иначе возвращает b.
Пример:
const name = username ?? "Аноним";
- Если username =
null→"Аноним" - Если username =
""→""(не null, значит, возвращается пустая строка) - Если username =
0→0
Когда использовать ???
При работе с настройками:
const timeout = config.timeout ?? 5000;
При обработке пользовательского ввода:
const age = user.age ?? 0; // если age не задан — 0, но если 0 — оставить
Оператор присваивания нулевого слияния: ??= - еще один современный оператор (JS, C#), позволяющий присвоить значение только если текущее — null или undefined.
a ??= b;
Если a — null или undefined, присвоить b. Иначе — ничего не делать.
Пример
let cache;
cache ??= initializeCache(); // вызовется только при первом обращении
Эквивалент
if (a == null) {
a = b;
}
Форма оператора — это не просто стиль. Она влияет на читаемость, краткость и семантику.
Операторы-символы используют специальные символы для обозначения действий. Это как раз +, -, *, /, &, %, |, ^, ~, !, <, >, =, +=, ==, ===, ?, :, ., [], (), {}. Они краткие, универсальные, знакомые всем, интуитивно понятны благодаря математике, однако могут быть неочевидными, и сложны для новичков.
Операторы-слова (ключевые слова) используют ключевые слова вместо символов. Чаще в языках, ориентированных на читаемость. Это, к примеру, and, or, not, is, is not, in, not in, as.
Некоторые операторы — на грани между символами и словами. К примеру, ==, означает «равно», а => это стрелочная функция (лямбда), читается как «тогда», «возвращает».